""" TODO:
def test_read_vector(self):
    b = Block.from_byte(ord('a'), 1024)
    def callbcak(dev: Device, bv):
        print(f"dev is {dev}")
    bv = BioVec(callback, dev, [(b, offset1, size), (b, offset2, size),], callback)

    
    device.commit(bv)
"""


from __future__ import annotations
from abc import ABC, abstractmethod
from block import Block
from utility import BitUtility 
from typing import Callable

class Device(ABC):
    def __init__(self, name: str|None):
        self.__name = name

    def name(self) -> str | None:
        return self.__name
    def set_name(self, name):
        self.__name = name

    def __repr__(self):
        return f"Device=> name:[{self.__name}]"


class BlockDevice(Device):
    def __init__(self, size: int, name: str|None = None):
        super().__init__(name)
        self.__block_size = BitUtility.align_greater_equal(size, 512)

    def block_size(self) -> int: # native block size
        return self.__block_size


    @abstractmethod
    def number_of_block(self) -> int:
        pass
    def number_of_bytes(self) -> int:
        return self.number_of_block() * self.block_size()
    

    @abstractmethod
    def seek(self, offset: int, whence: int = 0) -> int:
        pass

    def write_seq(self, frm: Block) -> int:
        return self._multi_io_seq(frm, self.native_write)
    def read_seq(self, to: Block) -> int:
        return self._multi_io_seq(to, self.native_read)

    def write_seq_blocks(self, frm_blocks: list[Block]) -> int:
        nbytes = 0
        for b in frm_blocks:
            nbytes = nbytes + self.write_seq(b)
        return nbytes
    def read_seq_blocks(self, to_blocks: list[Block]) -> int:
        nbytes = 0
        for b in to_blocks:
            nbytes = nbytes + self.read_seq(b)
        return nbytes


    def pwrite_seq(self, frm: Block, to: int) -> int:
        self.seek(to)
        return self.write_seq(frm)
    def pread_seq(self, frm: int, to: Block) -> int:
        self.seek(frm)
        return self.read_seq(to)

    def pwrite_seq_blocks(self, frm_blocks: list[Block], to: int) -> int:
        self.seek(to)
        return self.write_seq_blocks(frm_blocks)
    def pread_seq_blocks(self, frm: int, to_blocks: list[Block]) -> int:
        self.seek(frm)
        return self.read_seq_blocks(to_blocks)


    @abstractmethod
    def native_write(self, frm: memoryview) -> int:
        assert len(frm) == self.block_size()
        pass
    @abstractmethod
    def native_read(self, to: memoryview) -> int:
        assert len(to) == self.block_size()
        pass

    def _multi_io_seq(self, block: Block, cb_io: Callable[[memoryview], int]) -> int:
        if block.size() < self.block_size() or (block.size() % self.block_size()) != 0:
            raise ValueError(f"write error, length of block:{block.size()} is not aligned to block size of device:{self.block_size()}")

        times = int(block.size() / self.block_size())
        #print(f"write from mem:{block.size()} to device:{self.block_size()}, times:{times}")
        mv = memoryview(block.data())
        for i in range(times):
            cur = i * self.block_size()
            #print("write slice [{}->{})".format(cur, cur + self.block_size()))
            nwrite = cb_io(mv[cur: cur + self.block_size()])
            if nwrite != self.block_size():
                raise Exception("TODO: write to device:[{}] slice:[{}-{}) Error".format(self.name(), cur, cur + self.block_size()))
        return block.size()

class MockFloppyDiskDriver(BlockDevice):
    def __init__(self, path: str = "floppy.img"):
        super().__init__(512, "floppy disk driver")
        self.__impl = BinaryFileImpl(path)

    def __del__(self):
        self.__impl = None

    def number_of_block(self) -> int:
        return 80 * 18 * 2 #80-track  18-sector  2-doule-side

    def seek(self, offset: int, whence: int = 0) -> int:
        #TODO: err check
        return self.__impl.seek(offset, whence)

    def native_write(self, frm: memoryview) -> int:
        assert len(frm) == self.block_size()
        return self.__impl.write(frm)
    def native_read(self, to: memoryview) -> int:
        #TODO: replace with readinto
        assert len(to) == self.block_size()
        return self.__impl.read(to)

    def __repr__(self):
        return f"MockFloppyDiskDriver=>block_size:[{self.block_size()}], nblocks:[{self.number_of_block()}], impl:[{self.__impl}]"


class BinaryFileImpl(object):
    def __init__(self, path: str):
        self.__path = path
        self.__file = None
        self.__file = open(self.__path, "r+b")

    def __del__(self):
        if self.__file is not None:
            self.__file.close()
            self.__file = None

    def seek(self, offset: int, whence: int = 0) -> int:
        return self.__file.seek(offset, whence)
    def read(self, to: memoryview) -> int:
        to[:] = self.__file.read(len(to))
        return len(to)
    def write(self, frm: memoryview) -> int:
        return self.__file.write(frm)

    def __repr__(self):
        return f"BinaryFileImpl=>path:[{self.__path}], file:[{self.__file}]"

import unittest

class TestMockFloppyDiskDriver(unittest.TestCase):
    def setUp(self):
        #TODO: assert floppy.img is exist
        self.fdd = MockFloppyDiskDriver()

    def tearDown(self):
        self.fdd = None

    def test_meta(self):
        szblock = self.fdd.block_size()
        self.assertEqual(512, szblock)

        nbytes = self.fdd.number_of_bytes()
        self.assertEqual(80 * 18 * 2 * 512, nbytes)

        nblocks = self.fdd.number_of_block()
        self.assertEqual(80 * 18 * 2, nblocks)

    def test_native_write_read(self):
        b = Block.from_byte(ord('a'), 4096)
        mvb = memoryview(b.data())
        szblock = self.fdd.block_size()
        nwrite = self.fdd.native_write(mvb[0:szblock])
        self.assertEqual(szblock, nwrite)

        self.fdd.seek(0)
        nread = self.fdd.native_read(mvb[szblock: 2 * szblock])
        self.assertEqual(szblock, nread)
        self.assertEqual(ord('a'), b.data()[szblock])

    def test_write_read_seq(self):
        b = Block.from_byte(ord('a'), 4096)

        nwrite = self.fdd.write_seq(b)
        self.assertEqual(b.size(), nwrite)

        b.data()[1] = ord('b')
        nwrite = self.fdd.write_seq(b)
        self.assertEqual(b.size(), nwrite)

        self.fdd.seek(0)
        
        nread = self.fdd.read_seq(b)
        self.assertEqual(b.size(), nread)
        self.assertEqual(ord('a'), b.data()[0])

        nread = self.fdd.read_seq(b)
        self.assertEqual(b.size(), nread)
        self.assertEqual(ord('a'), b.data()[0])
        self.assertEqual(ord('b'), b.data()[1])

    def test_pwrite_pread_seq(self):
        b = Block.from_byte(ord('a'), 4096)

        offset = 1024
        nwrite = self.fdd.pwrite_seq(b, offset)
        self.assertEqual(b.size(), nwrite)

        b.data()[1] = ord('b')
        offset = offset + b.size()
        nwrite = self.fdd.pwrite_seq(b, offset)
        self.assertEqual(b.size(), nwrite)

        offset = 1024
        nread = self.fdd.pread_seq(offset, b)
        self.assertEqual(b.size(), nread)
        self.assertEqual(ord('a'), b.data()[0])

        offset = offset + b.size()
        nread = self.fdd.pread_seq(offset, b)
        self.assertEqual(b.size(), nread)
        self.assertEqual(ord('a'), b.data()[0])
        self.assertEqual(ord('b'), b.data()[1])

    def test_write_read_seq_blocks(self):
        self.fdd.seek(0)

        b = Block.from_byte(ord('a'), 1024)
        blocks = [b,] #Blocks().append(b)

        nwrite = self.fdd.write_seq_blocks(blocks)
        self.assertEqual(b.size(), nwrite)

        b.data()[1] = ord('b')
        nwrite = self.fdd.write_seq_blocks(blocks)
        self.assertEqual(b.size(), nwrite)

        self.fdd.seek(0)
        
        nread = self.fdd.read_seq_blocks(blocks)
        self.assertEqual(b.size(), nread)
        self.assertEqual(ord('a'), b.data()[0])

        nread = self.fdd.read_seq_blocks(blocks)
        self.assertEqual(b.size(), nread)
        self.assertEqual(ord('a'), b.data()[0])
        self.assertEqual(ord('b'), b.data()[1])
        
    def test_pwrite_pread_seq_blocks(self):
        b = Block.from_byte(ord('a'), 1024)
        blocks = [b,]

        offset = 1024
        nwrite = self.fdd.pwrite_seq_blocks(blocks, offset)
        self.assertEqual(b.size(), nwrite)

        b.data()[1] = ord('b')
        offset = offset + b.size()
        nwrite = self.fdd.pwrite_seq_blocks(blocks, offset)
        self.assertEqual(b.size(), nwrite)

        offset = 1024
        nread = self.fdd.pread_seq_blocks(offset, blocks)
        self.assertEqual(b.size(), nread)
        self.assertEqual(ord('a'), b.data()[0])

        offset = offset + b.size()
        nread = self.fdd.pread_seq_blocks(offset, blocks)
        self.assertEqual(b.size(), nread)
        self.assertEqual(ord('a'), b.data()[0])
        self.assertEqual(ord('b'), b.data()[1])

